// 
//  Copyright © 2009 Jiří Zárevúcky <zarevucky.jiri@gmail.com>
// 
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU Affero General Public License as
//  published by the Free Software Foundation, either version 3 of the
//  License, or (at your option) any later version.
// 
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU Affero General Public License for more details.
// 
//  You should have received a copy of the GNU Affero General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
// 
// 


using System;
using System.Text;
using System.Collections;
using System.Collections.Specialized;
using System.Collections.Generic;

using Galaxium.Protocol.Xmpp.Library.Xml;
using Galaxium.Protocol.Xmpp.Library.Core;

namespace Galaxium.Protocol.Xmpp.Library.Extensions
{
	public enum FormAction { Form, Submit, Result, Cancel }
	
	public class DataFormSubmitArgs
	{
		public DataForm Form { get; private set; }
		public StanzaError Error { get; set; }
		
		public DataFormSubmitArgs (DataForm form)
		{
			Form = form;
		}
	}
	
	public class DataForm: IEnumerable<DataField>, IComparable<DataForm>
	{
		public event EventHandler SubmitSucceeded;
		public event EventHandler<StanzaErrorEventArgs> SubmitFailed;
		
		private Action<DataFormSubmitArgs> _submit_action;
		
		public DataForm (FormAction action, Action<DataFormSubmitArgs> submit_action)
		{
			Action = action;
			_fields = new List<DataField> ();
			_indices = new SortedDictionary<string, int> (Disco.OctetCollationComparer.Instance);
			_submit_action = submit_action;
		}
		
		public DataForm (Element x, Action<DataFormSubmitArgs> submit_action)
			:this (FormAction.Form, submit_action)
		{
			if (x.Name != "x" || x.Namespace != Namespaces.DataForms)
				throw new ArgumentException ();

			try { Action = (FormAction) Enum.Parse (typeof (FormAction), x ["type"], true); }
			catch { Action = FormAction.Form; }
				
			foreach (var child in x) {
				switch (child.Name) {
				case "title": Title = child.Text; break;
				case "instructions": Instructions = child.Text; break;
				case "field": AddField (new DataField (child)); break;
				}
			}
		}
		
		private bool _submitted;
		private List<DataField> _fields;
		private SortedDictionary<string, int> _indices;
		
		public string Title { get; set; }
		
		public string Instructions { get; set; }
		
		public FormAction Action { get; set; }
		
		public string Type { get; set; }
		
		public DataField this [string index] {
			get { return GetField (index); }
		}

		public bool ContainsField (string name)
		{
			return _indices.ContainsKey (name);
		}
		
		public DataField GetField (string name)
		{
			try {
				return _fields [_indices [name]];
			}
			catch {
				throw new ArgumentOutOfRangeException ();
			}
		}
		
		#region Convenience overloads for adding fields
		
		public void AddFixedField (string text)
		{
			var field = new DataField (null, DataFieldType.Fixed);
			field.SetString (text);
			AddField (field);
		}
		
		public void AddField (string name, string value, string label, bool multiline)
		{
			var type = multiline ? DataFieldType.TextMulti : DataFieldType.TextSingle;
			var field = new DataField (name, type);
			field.Label = label;
			field.SetString (value);
			AddField (field);
		}
		
		public void AddField (string name, string value, string label)
		{
			AddField (name, value, label, false);
		}
		
		public void AddField (string name, bool value, string label)
		{
			var field = new DataField (name, DataFieldType.Boolean);
			field.SetBool (value);
			field.Label = label;
			AddField (field);
		}
		
		public void AddField (string name, JabberID value, string label)
		{
			var field = new DataField (name, DataFieldType.JidSingle);
			field.SetJid (value);
			field.Label = label;
			AddField (field);
		}
		
		public void AddSection (string label)
		{
			var field = new DataField ("x-format-section-" + label, DataFieldType.Hidden);
			field.Label = label;
			AddField (field);
			field = new DataField (null, DataFieldType.Fixed);
			AddField (field);
			field = new DataField (null, DataFieldType.Fixed);
			field.SetString (label);
			AddField (field);
		}
		
		#endregion
		
		public void AddField (DataField field)
		{
			if (field.Identifier == "FORM_TYPE" && field.Type == DataFieldType.Hidden) {
				Type = field.GetString ();
				return;
			}
			if (field.Identifier != null) {
				if (_indices.ContainsKey (field.Identifier))
					throw new ArgumentException ("Field with the same name already exists.");
				_indices.Add (field.Identifier, _fields.Count);				
			}
			_fields.Add (field);
		}
		
		public Element ToXml (bool submit)
		{
			var x = new Element ("x", Namespaces.DataForms);
			x ["type"] = submit ? "submit" : "form";
			
			if (!submit) {
				if (!String.IsNullOrEmpty (Title))
					x.AppendChild (new Element ("title").SetText (Title));
				if (!String.IsNullOrEmpty (Instructions))
					x.AppendChild (new Element ("instructions").SetText (Instructions));
				if (!String.IsNullOrEmpty (Type)) {
					var f = new DataField ("FORM_TYPE", DataFieldType.Hidden);
					f.SetString (Type);
					x.AppendChild (f.ToXml (false));
				}
			}
			
			foreach (var field in _fields) {
				var e = field.ToXml (submit);
				if (e != null) x.AppendChild (e);
			}
			
			return x;
		}
		
		public void Submit ()
		{
			if (_submitted)
				throw new InvalidOperationException ("Form was already submitted.");
			_submitted = true;
			var args = new DataFormSubmitArgs (this);
			_submit_action.BeginInvoke (args, SubmitFinished, args);
		}
		
		private void SubmitFinished (IAsyncResult ar)
		{
			_submit_action.EndInvoke (ar);
			var error = (ar.AsyncState as DataFormSubmitArgs).Error;
			if (error == null)
				SubmitSucceeded (this, EventArgs.Empty);
			else
				SubmitFailed (this, new StanzaErrorEventArgs (error));
		}
		
		public IEnumerator<DataField> GetEnumerator ()
		{
			return _fields.GetEnumerator ();
		}
		
		IEnumerator IEnumerable.GetEnumerator ()
		{
			return GetEnumerator ();
		}

		public int CompareTo (DataForm other)
		{
			return Disco.OctetCollationComparer.Instance.Compare (this.Type, other.Type);
		}
		
		public string GenerateVerificationString ()
		{
			var sb = new StringBuilder ();
			
			sb.Append (Type);
			sb.Append ('<');
			
			foreach (var index in _indices.Values)
				sb.Append (_fields [index].GetVerificationString ());
			
			return sb.ToString ();
		}
	}
}
